﻿
using EmperialApps.WeatherSpark.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Threading;

namespace EmperialApps.WeatherSpark.Agent {

    /// <summary>Provides a communication channel between the app and the background agent.</summary>
    public static class SharedStorage {

        public delegate T Read<T>( Stream stream );

        public delegate void Write<T>( Stream stream, T data );

        public delegate T Update<T, U>( bool wasRead, T read, U update );


        /// <summary>Gets the name of the background agent.</summary>
        public const string AgentName = "WeatherSpark.WPAgent";

        /// <summary>Gets the name of the file where errors in the background agent are recorded.</summary>
        public const string ErrorFile = "LittleWatson.Agent.txt";

        /// <summary>Gets the name of the directory where forecasts are saved.</summary>
        public const string ForecastsDirectory = "forecasts";

        /// <summary>Gets the name of the drive where tile images are saved.</summary>
        public const string TileImageDrive = "isostore:/";

        /// <summary>Gets the name of the directory where tile images are saved.</summary>
        public const string TileImageDirectory = TileImageDrive + "Shared/ShellContent/";

        /// <summary>Gets the identifier used for small tile images.</summary>
        public const string SmallTileImagePathModifier = "s";

        /// <summary>Gets the identifier used for flip tile images.</summary>
        public const string FlipTileImagePathModifier = "f";

        /// <summary>Gets the identifier used for wide tile images.</summary>
        public const string WideTileImagePathModifier = "w";

        /// <summary>Gets the identifier used for wide flip tile images.</summary>
        public const string WideFlipTileImagePathModifier = WideTileImagePathModifier + FlipTileImagePathModifier;

        /// <summary>Gets the URI of the tile background logo image.</summary>
        public static readonly Uri BackgroundImage = new Uri( "/Assets/Background.png", UriKind.Relative );


        /// <summary>Gets the serialized name of the specified location.</summary>
        public static string GetName( this Coordinate location ) {
            return location.ToString( display: false );
        }

        /// <summary>Gets the serialized name of the specified location.</summary>
        public static Uri GetTileImageSource( Coordinate location, Units units, ForecastDisplayMode mode, string modifier = null ) {
            if( mode == ForecastDisplayMode.None )
                return BackgroundImage;

            string name = location.GetTileName( units, mode );
            string path = TileImageDirectory + name + modifier + ".png";
            return new Uri( path, UriKind.Absolute );
        }

        /// <summary>Determines whether a file exists at the specified location.</summary>
        public static bool TestImageSourcePath( Uri imageSource ) {
            if( object.ReferenceEquals( imageSource, BackgroundImage ) )
                return true;

            string path = GetFilePath( imageSource );
            using( var store = IsolatedStorageFile.GetUserStoreForApplication( ) )
                return store.FileExists( path );
        }

        /// <summary>Creates a tile image based on the specified settings.</summary>
        public static TileImageBase CreateTileImage( string pathModifier, string title, Units units, ForecastDisplayMode mode, Forecast forecast, uint backgroundColor ) {
            TileImageBase tileImage;
            switch( pathModifier ) {
                case SharedStorage.WideFlipTileImagePathModifier:
                    tileImage = new TileWideFlipImage( );
                    break;
                case SharedStorage.WideTileImagePathModifier:
                    tileImage = new TileWideImage( );
                    break;
                case SharedStorage.FlipTileImagePathModifier:
                    tileImage = new TileFlipImage( );
                    break;
                default:
                    tileImage = new TileImage( );
                    break;
            }

            tileImage.PathModifier = pathModifier;
            tileImage.Title = title;
            tileImage.Units = units;
            tileImage.DisplayMode = mode;
            tileImage.Forecast = forecast;
            tileImage.BackgroundColor = Display.ToColor( backgroundColor );

            return tileImage;
        }

        /// <summary>Saves the specified tile image to a file.</summary>
        public static Uri SaveTileImage( TileImageBase tileImage ) {
            Coordinate location = tileImage.Forecast.Location;
            Units units = tileImage.Units.GetValueOrDefault( );
            ForecastDisplayMode mode = tileImage.DisplayMode.GetValueOrDefault( );
            Uri imageSource = GetTileImageSource( location, units, mode, tileImage.PathModifier );

            if( mode != ForecastDisplayMode.None ) {
                string path = GetFilePath( imageSource );
                try {
                    AtomicWrite( path, TileImageBase.SaveImage, tileImage );
                }
                catch( OutOfMemoryException ) {
                    AtomicDelete( path );
                }
            }

            return imageSource;
        }

        /// <summary>Deletes the specified tile image file.</summary>
        public static bool DeleteTileImage( Uri imageSource ) {
            string path = GetFilePath( imageSource );
            return AtomicDelete( path );
        }


        /// <summary>Combines the specified forecasts, saving data through one day's history values.</summary>
        public static Forecast CombineForecasts( Forecast existing, Forecast update ) {
            DateTimeOffset now = DateTimeOffset.UtcNow.ToOffset( update.Start.Offset );
            DateTimeOffset yesterday = now.GetDateOffset( ).AddDays( -1 );
            int maximumHours = (int)update.End( ).HoursFrom( yesterday );

            // If forecast update has less than a day of new values, default to a week of data plus a day of history.
            if( maximumHours <= ConvertValue.HoursPerDay )
                maximumHours = 8 * ConvertValue.HoursPerDay;

            Forecast combined = Forecast.Combine( existing, update, maximumHours );
            return combined;
        }

        /// <summary>Saves the specified forecast to a location file.</summary>
        public static void SaveForecast( Forecast forecast ) {
            string path = GetFilePath( forecast.Location );
            AtomicWrite( path, Forecast.Save, forecast );
        }

        /// <summary>Loads the forecast from the specified location file.</summary>
        public static bool TryLoadForecast( Coordinate location, out Forecast forecast ) {
            string path = GetFilePath( location );
            return AtomicRead( path, Forecast.Load, out forecast );
        }

        /// <summary>Loads and updates any existing forecast before saving the result to a location file.</summary>
        public static Forecast UpdateForecast<U>( Coordinate location, Update<Forecast, U> update, U updateData ) {
            string path = GetFilePath( location );
            return AtomicUpdate( path, Forecast.Load, update, Forecast.Save, updateData );
        }

        /// <summary>Loads and combines any existing forecast before saving the result to a location file.</summary>
        public static Forecast UpdateForecast( Forecast forecast ) {
            return UpdateForecast( forecast.Location, ForecastUpdate, forecast );
        }

        /// <summary>Deletes the specified location file.</summary>
        public static bool DeleteForecast( Coordinate location ) {
            string path = GetFilePath( location );
            return AtomicDelete( path );
        }


        /// <summary>Saves the specified tile information for use by the background agent.</summary>
        public static void SaveForecastInfo( ForecastInfo[] forecastInfo ) {
            AtomicWrite( AgentFile, ForecastInfo.Save, forecastInfo );
        }

        /// <summary>Loads and updates any existing forecast before saving the result to a location file.</summary>
        public static ForecastInfo[] UpdateForecastInfo<U>( Update<ForecastInfo[], U> update, U updateData ) {
            return AtomicUpdate( AgentFile, ForecastInfo.Load, update, ForecastInfo.Save, updateData );
        }

        /// <summary>Loads the tile information used by the background agent.</summary>
        public static bool TryLoadForecastInfo( out ForecastInfo[] forecastInfo ) {
            try {
                return AtomicRead( AgentFile, ForecastInfo.Load, out forecastInfo );
            }
            catch( Exception ex ) {
                typeof( SharedStorage ).Log( "Load failure: " + ex );
                forecastInfo = null;
                return false;
            }
        }


        /// <summary>Gains exclusive access to the specified file before performing the write operation.</summary>
        public static void AtomicWrite<T>( string path, Write<T> write, T data ) {
            Mutex mutex = GetMutex( path );
            bool acquired = false;
            try {
                TryWaitOne( mutex, out acquired ); // Allow overwriting potentially corrupted state.
                using( var store = IsolatedStorageFile.GetUserStoreForApplication( ) )
                    WriteCore<T>( store, path, write, data );
            }
            finally {
                TryReleaseMutex( mutex, acquired );
            }
        }

        /// <summary>Gains exclusive access to the specified file before performing the read operation.</summary>
        public static bool AtomicRead<T>( string path, Read<T> read, out T data ) {
            Mutex mutex = GetMutex( path );

            bool acquired = false;
            try {
                if( !TryWaitOne( mutex, out acquired ) ) {
                    // Deny access to potentially corrupted state.
                    TryReleaseMutex( mutex, acquired );
                    data = default( T );
                    return false;
                }

                using( var store = IsolatedStorageFile.GetUserStoreForApplication( ) )
                    return ReadCore<T>( store, path, read, out data );
            }
            finally {
                TryReleaseMutex( mutex, acquired );
            }
        }

        /// <summary>Gains exclusive access to the specified file before performing the update operation.</summary>
        public static T AtomicUpdate<T, U>( string path, Read<T> read, Update<T, U> update, Write<T> write, U updateData ) {
            Mutex mutex = GetMutex( path );
            bool acquired = false;
            try {
                if( !TryWaitOne( mutex, out acquired ) ) // Only allow overwriting potentially corrupted state.
                    read = null;

                using( var store = IsolatedStorageFile.GetUserStoreForApplication( ) )
                    return UpdateCore<T, U>( store, path, read, update, write, updateData );
            }
            finally {
                TryReleaseMutex( mutex, acquired );
            }
        }

        /// <summary>Gains exclusive access to the specified file before performing the delete operation.</summary>
        public static bool AtomicDelete( string path ) {
            Mutex mutex = GetMutex( path );
            bool acquired = false;
            try {
                TryWaitOne( mutex, out acquired ); // Allow deleting potentially corrupted state.
                using( var store = IsolatedStorageFile.GetUserStoreForApplication( ) )
                    return DeleteCore( store, path );
            }
            finally {
                TryReleaseMutex( mutex, acquired );
            }
        }


        #region Private Members

        private const string AgentFile = AgentName + ".bin";

        private static readonly Dictionary<string, Mutex> _fileMutexes = new Dictionary<string, Mutex>( );

        private static Mutex GetMutex( string path ) {
            Mutex mutex;
            string name = path.Replace( '\\', '|' );
            if( !_fileMutexes.TryGetValue( name, out mutex ) )
                _fileMutexes[path] = mutex = new Mutex( false, name );

            return mutex;
        }

        private static bool TryWaitOne( Mutex mutex, out bool acquired ) {
            try {
                acquired = mutex.WaitOne( );
                return true;
            }
            catch( Exception ex ) {
                // (Manual check for "AbandonedMutexException" message, since type is not available on WP7.)
                if( ex.Message == "The wait completed due to an abandoned mutex." ) {
                    typeof( SharedStorage ).Log( "Acquired abandoned mutex." );
                    acquired = true;
                    return false;
                }

                acquired = false;
                throw;
            }
        }

        private static void TryReleaseMutex( Mutex mutex, bool acquired ) {
            if( acquired )
                mutex.ReleaseMutex( );
        }

        private static string GetFilePath( Uri imageSource ) {
            return imageSource.AbsolutePath;
        }

        private static string GetFilePath( Coordinate location ) {
            string name = location.GetName( );
            return Path.Combine( ForecastsDirectory, name );
        }

        private static Forecast ForecastUpdate( bool wasRead, Forecast read, Forecast update ) {
            return wasRead
                 ? CombineForecasts( read, update )
                 : update;
        }


        private static void WriteCore<T>( IsolatedStorageFile store, string path, Write<T> write, T data ) {
            if( data == null )
                return;

            string directory = Path.GetDirectoryName( path );
            if( directory.Length > 0 )
                store.CreateDirectory( directory );

            using( var file = store.OpenFile( path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None ) )
                write( file, data );
        }

        private static bool ReadCore<T>( IsolatedStorageFile store, string path, Read<T> read, out T data ) {
            if( read != null && store.FileExists( path ) ) {
                using( var file = store.OpenFile( path, FileMode.Open, FileAccess.Read, FileShare.Read ) )
                    data = read( file );
                return true;
            }
            else {
                data = default( T );
                return false;
            }
        }

        private static T UpdateCore<T, U>( IsolatedStorageFile store, string path, Read<T> read, Update<T, U> update, Write<T> write, U updateData ) {
            T readData;
            bool wasRead = ReadCore<T>( store, path, read, out readData );
            T writeData = update( wasRead, readData, updateData );

            WriteCore<T>( store, path, write, writeData );
            return writeData;
        }

        private static bool DeleteCore( IsolatedStorageFile store, string path ) {
            if( store.FileExists( path ) ) {
                store.DeleteFile( path );
                return true;
            }
            else {
                return false;
            }
        }

        #endregion

    }

}
